/**
 * filter xss
 *
 * @author Zongmin Lei<leizongmin@gmail.com>
 */
import * as _ from '../cssFilter/_util'
import * as DEFAULT from './default'
import { CSSFilter } from '../cssFilter'
import { parseTag, parseAttr } from './parser'

/**
 * returns `true` if the input value is `undefined` or `null`
 *
 * @param {Object} obj
 * @return {Boolean}
 */
function isNull(obj) {
  return obj === undefined || obj === null
}

/**
 * get attributes for a tag
 *
 * @param {String} html
 * @return {Object}
 *   - {String} html
 *   - {Boolean} closing
 */
function getAttrs(html) {
  var i = _.spaceIndex(html)
  if(i === -1) {
    return {
      html: '',
      closing: html[html.length - 2] === '/',
    }
  }
  html = _.trim(html.slice(i + 1, -1))
  var isClosing = html[html.length - 1] === '/'
  if(isClosing) html = _.trim(html.slice(0, -1))
  return {
    html: html,
    closing: isClosing,
  }
}

/**
 * shallow copy
 *
 * @param {Object} obj
 * @return {Object}
 */
function shallowCopyObject(obj) {
  var ret = {}
  for(var i in obj) {
    ret[i] = obj[i]
  }
  return ret
}


// https://github.com/leizongmin/js-xss/blob/master/README.zh.md
export const defaultXssOptions = {
  whiteList: { // 白名单为空，表示过滤所有标签
    img: ['src', 'class'],
    span: ['class', 'style'],
    div: ['class', 'style'],
    a: ['class', 'style'],
    b: ['class', 'style'],
    i: ['class', 'style'],
    strong: ['class', 'style'],
    sup: ['class', 'style'],
    sub: ['class', 'style'],
    ul: ['class', 'style'],
    li: ['class', 'style']
  },
  stripIgnoreTag: true, // 过滤所有非白名单标签的HTML
  stripIgnoreTagBody: ['script'] // script标签较特殊，需要过滤标签中间的内容
}

/**
 * XSSFilter class
 *
 * @param {Object} options
 *        whiteList, onTag, onTagAttr, onIgnoreTag,
 *        onIgnoreTagAttr, safeAttrValue, escapeHtml
 *        stripIgnoreTagBody, allowCommentTag, stripBlankChar
 *        css{whiteList, onAttr, onIgnoreAttr} `css=false` means don't use `cssfilter`
 */
export function XSSFilter(options= defaultXssOptions) {
  options = shallowCopyObject(options || {})

  if(options.stripIgnoreTag) {
    if(options.onIgnoreTag) {
      console.error(
        'Notes: cannot use these two options "stripIgnoreTag" and "onIgnoreTag" at the same time'
      )
    }
    options.onIgnoreTag = DEFAULT.onIgnoreTagStripAll
  }

  options.whiteList = options.whiteList || DEFAULT.whiteList
  options.onTag = options.onTag || DEFAULT.onTag
  options.onTagAttr = options.onTagAttr || DEFAULT.onTagAttr
  options.onIgnoreTag = options.onIgnoreTag || DEFAULT.onIgnoreTag
  options.onIgnoreTagAttr = options.onIgnoreTagAttr || DEFAULT.onIgnoreTagAttr
  options.safeAttrValue = options.safeAttrValue || DEFAULT.safeAttrValue
  options.escapeHtml = options.escapeHtml || DEFAULT.escapeHtml
  this.options = options

  if(options.css === false) {
    this.cssFilter = false
  } else {
    options.css = options.css || {}
    this.cssFilter = new CSSFilter(options.css)
  }
}

/**
 * start process and returns result
 *
 * @param {String} html
 * @return {String}
 */
XSSFilter.prototype.process = function(html) {
  // compatible with the input
  html = html || ''
  html = html.toString()
  if(!html) return ''

  var me = this
  var options = me.options
  var whiteList = options.whiteList
  var onTag = options.onTag
  var onIgnoreTag = options.onIgnoreTag
  var onTagAttr = options.onTagAttr
  var onIgnoreTagAttr = options.onIgnoreTagAttr
  var safeAttrValue = options.safeAttrValue
  var escapeHtml = options.escapeHtml
  var cssFilter = me.cssFilter

  // remove invisible characters
  if(options.stripBlankChar) {
    html = DEFAULT.stripBlankChar(html)
  }

  // remove html comments
  if(!options.allowCommentTag) {
    html = DEFAULT.stripCommentTag(html)
  }

  // if enable stripIgnoreTagBody
  var stripIgnoreTagBody = false
  if(options.stripIgnoreTagBody) {
    var stripIgnoreTagBody = DEFAULT.StripTagBody(
      options.stripIgnoreTagBody,
      onIgnoreTag
    )
    onIgnoreTag = stripIgnoreTagBody.onIgnoreTag
  }

  var retHtml = parseTag(
    html,
    function(sourcePosition, position, tag, html, isClosing) {
      var info = {
        sourcePosition: sourcePosition,
        position: position,
        isClosing: isClosing,
        isWhite: whiteList.hasOwnProperty(tag),
      }

      // call `onTag()`
      var ret = onTag(tag, html, info)
      if(!isNull(ret)) return ret

      if(info.isWhite) {
        if(info.isClosing) {
          return '</' + tag + '>'
        }

        var attrs = getAttrs(html)
        var whiteAttrList = whiteList[tag]
        var attrsHtml = parseAttr(attrs.html, function(name, value) {
          // call `onTagAttr()`
          var isWhiteAttr = _.indexOf(whiteAttrList, name) !== -1
          var ret = onTagAttr(tag, name, value, isWhiteAttr)
          if(!isNull(ret)) return ret

          if(isWhiteAttr) {
            // call `safeAttrValue()`
            value = safeAttrValue(tag, name, value, cssFilter)
            if(value) {
              return name + '="' + value + '"'
            } else {
              return name
            }
          } else {
            // call `onIgnoreTagAttr()`
            var ret = onIgnoreTagAttr(tag, name, value, isWhiteAttr)
            if(!isNull(ret)) return ret
            return
          }
        })

        // build new tag html
        var html = '<' + tag
        if(attrsHtml) html += ' ' + attrsHtml
        if(attrs.closing) html += ' /'
        html += '>'
        return html
      } else {
        // call `onIgnoreTag()`
        var ret = onIgnoreTag(tag, html, info)
        if(!isNull(ret)) return ret
        return escapeHtml(html)
      }
    },
    escapeHtml
  )

  // if enable stripIgnoreTagBody
  if(stripIgnoreTagBody) {
    retHtml = stripIgnoreTagBody.remove(retHtml)
  }

  return retHtml
}
